class IRP
  require 'pathname'
  require 'thread'
  require 'yaml'
  require 'java'
  include_class 'java.awt.Frame'
  include_class 'java.awt.Color'
  include_class 'java.awt.FileDialog'
  include_class 'java.awt.GraphicsEnvironment'
  include_class 'java.awt.event.WindowAdapter'
  include_class 'java.awt.event.KeyAdapter'
  include_class 'java.awt.event.KeyEvent'
  include_class 'java.lang.System'
  
  APP_ROOT = Pathname.new(__FILE__).dirname.expand_path
  $LOAD_PATH << APP_ROOT + 'libraries'
  
  def initialize(opt)
    super()
    $irp = self
    
    @file = Pathname.new(opt[:file] || open_dialog)
    @source = @file.read
    @time = Time.now
    @reload_listener = ReloadListener.new
    @dispose_listener = DisposeListener.new
    @interval = opt[:interval]
    @on_update = opt[:reload] ? method(:reload) : method(:evaluate)
    
    set_working_directory
    load_processing
    create_frame(opt[:bgcolor], opt[:fullscreen], opt[:display])    
    create_sketch  
    start_observing if opt[:observe]    
    start_service if opt[:server]   
  end
  
  def open_dialog
    fd = FileDialog.new(Frame.new, 'Open', FileDialog::LOAD)
    fd.show
    System.exit(0) unless fd.getFile
    fd.getDirectory + fd.getFile
  end
  
  def set_working_directory
    user_dir = @file.dirname.expand_path.to_s
    Dir.chdir(user_dir)
    System.setProperty("user.dir", user_dir)
  end
  
  def load_processing
    config = YAML.load((APP_ROOT + 'config.yaml').read)
    p5home = Pathname.new(config['p5home'])
    require p5home + 'lib' + 'core.jar'
    include_class 'processing.core.PApplet'
    include_class 'processing.core.PMatrix'
    include_class 'processing.core.PImage'
    include_class 'processing.core.PFont'
    include_class 'processing.core.PConstants'
    load_all_libraries(p5home)
  end
  
  def load_all_libraries(p5home)
    require 'utils'
    (p5home + 'libraries').children.each do |f|
      load_library(f) if f.directory?
    end
  end
  
  def load_library(base)
    lib = base + 'library'
    jars = Dir.glob(base + '*.jar') + Dir.glob(lib + '*.jar')
    raise "Could not load the #{folder} library." if jars.length == 0
    jars.each {|jar| require jar}
    key = 'java.library.path'
    old_path = System.getProperty(key)
    new_path = [base, lib, old_path].join(java.io.File.pathSeparator)
    System.setProperty(key, new_path)
    klass = java.lang.Class
    if field = klass.forName("java.lang.ClassLoader").getDeclaredField("sys_paths")
      field.accessible = true
      field.set(klass.forName("java.lang.System").classLoader, nil)
    end
  end
  
  def create_frame(bgcolor, fullscreen, display)
    env = GraphicsEnvironment.getLocalGraphicsEnvironment
    begin
      device = env.getScreenDevices[display]
    rescue ArgumentError
      puts "Display #{display} does not exist, using the default display instead."
      device = env.getDefaultScreenDevice
    end
    
    @frame = Frame.new(device.getDefaultConfiguration)
    @frame.setTitle(File.basename(@file, ".rb"))
    @frame.setBackground(Color.new(bgcolor))
    @frame.setResizable(false)
    @frame.addWindowListener(Class.new(WindowAdapter) do
      def windowClosing(e)
        $irp.applet.exit
      end
    end.new)
    @frame.setLayout(nil)
    
    if fullscreen
      @frame.setUndecorated(true)
      device.setFullScreenWindow(@frame)
    else
      @frame.pack
    end
    @fullscreen = fullscreen
  end
  
  def start_observing 
    Thread.start do 
      loop do
        if @file.mtime > @time
          @time = @file.mtime
          @source = @file.read
          @on_update.call
        end
        sleep @interval
      end   
    end 
  end    
         
  def start_service   
    require "socket"       
    gate = TCPServer.open 5555
    loop do    
      sock = gate.accept  
      Thread.start do    
        code = sock.read  
        sock.close  
        self.instance_eval(code)  
      end
    end 
  end        
  
  def evaluate(src = @source)  
    begin       
      @klass.module_eval(src)
    rescue StandardError, SyntaxError
      warn $!   
    end
  end
  
  def reload
    remove_sketch
    GC.start
    create_sketch
  end      
  
  def create_sketch
    @klass = empty_class
    evaluate
    @applet = @klass.new
    @applet.init
    
    sleep(0.005) while @applet.defaultSize && !@applet.finished
    
    w, h = @applet.width, @applet.height
    if @fullscreen
      size = @frame.getSize
      @applet.setBounds((size.width - w) / 2,
                        (size.height - h) / 2,
                        w, h)
    else
      ins = @frame.getInsets
      l, t, r, b = ins.left, ins.top, ins.right, ins.bottom
      win_w = [w, 128].max + l + r
      win_h = [h, 128].max + t + b
      @frame.setSize(win_w, win_h)
      usable_win_h = win_h - t - b
      @applet.setBounds((win_w - w)/2,
                t + (usable_win_h - h)/2,
                w, h)
    end
    @frame.add(@applet)
    @frame.setVisible(true)
    @applet.requestFocus
    
    if defined? @applet.g.canvas
      @key_dispatcher = @applet.g.canvas
    else
      @key_dispatcher = @applet
    end
    @key_dispatcher.addKeyListener(@reload_listener)
    @applet.registerDispose(@dispose_listener)
  end
  
  def remove_sketch
    @key_dispatcher.removeKeyListener(@reload_listener)
    @key_dispatcher = nil
    @frame.remove(@applet)
    @applet.destroy
    @applet.unregisterDispose(@dispose_listener)
    @applet = nil
    @klass = nil
  end
  
  def empty_class
    Class.new(PApplet) do  
      def self.const_missing(id)   
        PConstants.const_get(id)
      end
      
      def method_missing(*args)
        PApplet.send(*args)
      end
      
      def print(obj)
        if obj.is_a? java.awt.Graphics
          super(obj)
        else
          PApplet.print(obj)
        end
      end  
      
      def isMousePressed 
        Java.java_to_primitive(java_class.field("mousePressed").value(java_object))
      end          
      alias :mouse_pressed? :isMousePressed
      
      def isKeyPressed   
        Java.java_to_primitive(java_class.field("keyPressed").value(java_object))
      end   
      alias :key_pressed? :isKeyPressed   
      
      def setup;end
      def draw;end
      def mouseDragged;end
      def mouseMoved;end
      def mouseReleased;end
      def mousePressed;end
      def mouseClicked;end
      def keyTyped;end
      def keyReleased;end
      def keyPressed;end
    end
  end
  
  def fullscreen?
    @fullscreen
  end
  
  def applet
    @applet
  end
  
  class ReloadListener < KeyAdapter
    def keyPressed(e)
      case e.getModifiers
      when 2, 4
        case e.getKeyCode
        when KeyEvent::VK_R then $irp.reload
        end
      end
    end
  end
  
  class DisposeListener
    require 'rplib.jar'
    include_class 'IPAppletAdapter'
    include IPAppletAdapter
    
    def dispose
      p = $irp.applet
      p.dispose if defined? p.dispose
    end
  end
end


require 'optparse'
options = {
  :interval => 0.1,
  :observe => true,
  :reload => false,
  :fullscreen => false,
  :display => 0,
  :bgcolor => 0x000000,
  :server => false
}
opt = OptionParser.new
opt.on('-i', '--interval=VAL') {|v| options[:interval] = [v.to_f, 0.01].max}
opt.on('-n', '--no-file-observe') {options[:observe] = false}
opt.on('-r', '--reload-always') {options[:reload] = true}
opt.on('-f', '--fullscreen') {options[:fullscreen] = true}
opt.on('-d', '--display=VAL') {|v| options[:display] = [v.to_i, 0].max}
opt.on('-b', '--bgcolor=VAL') {|v| options[:bgcolor] = [v.hex, 0].max}    
opt.on('-s', '--server') {|v| options[:server] = true}  
opt.parse!(ARGV)
options[:file] = File.expand_path(ARGV.first) if ARGV.first  
IRP.new(options)